王子子的成长之路

改善 Python 程序的 91 个建议读书笔记 3

建议 41:使用argparse处理命令行参数

处理命令行参数可以使用argsparse,也推荐更方便更高级的docopt进行处理。
docopt是根据常见的帮助信息定义了一套领域特定语言(DSL),并通过这个DSL Parser参数生成处理命令行参数的代码。

建议 42:使用pandas处理大型CSV文件

pandas作为python三大科学运算库之一的使用。

建议 43:一般情况下使用ElementTree解析xml格式文件

使用Beautifulsoup更好

建议 44:理解模块pickle优劣

序列化,简单来说就是把内存中的数据结构在不丢失其身份和类型信息的情况下转成对象的文本或二进制表示的过程。同类支持序列化的模块有pickle,json,marshal和shelve。

pickle是最通用的序列化模块,我们应该优先使用c语言实现的cPickle,速度比pickle快1000倍,区别是cPickle不能被继承。

pickle主要通过dump和load两种方法序列化与反序列化(存储与读取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import cPickle as pickle
# 序列化
my_data= {"name":"Python","type":"Language"}
fp = open("picklefile.dat","wb")
pickle.dump(my_data, fp)
fp.close
# 反序列化
fp = open("picklefile.dat", "rb")
out = pickle.load(fp)
```
pickle模块的优点:
1. 接口简单,容易使用。
2. 存储格式有平台通用型,在Linux和Windouws都可以使用,兼容性好。
3. 支持数据类型广泛,除了常规项,还包含能通过类的\__dict__或\__getstate__()方法返回的对象。
4. pickle是可扩展的,对于不可序列化的对象,也可以通过特殊方法来返回示例在被pickle时的状态。
5. 能够自动维护对象间的引用
pickle模块的限制:
* pickle不能保证操作的原子性。当错误发生时,可能部分数据已经被保存;如果对象处于深递归状态,那么可能超过python的最大递归深度,可以通过sys.setrecursionlimit()进行扩展。
* pickle存在安全性问题,为乳清提供了可能。
* pickle协议是python特定的,不同语言之间数据内容可能难以保障。
简单来说,对于需要存储的对象,使用pickle,另外很重要的一点,**dat文件用pickle模块来读**。
### 建议 45:序列化的另一个不错的选择 -- JSON
cJson比python自身的json要快250
JSON的优势:
1. 使用简单,支持多种数据类型(集合、列表、字典、关联数组等等)
2. 存储格式可读性更友好,易于修改
3. 支持跨平台跨语言操作,所占空间更小
4. 具有较强扩展性
json的速度比pickle略慢。
**json不支持序列化dateime**
### 建议 46:使用 traceback 获取栈信息
当发生异常,开发人员往往需要看到现场信息,trackback 模块可以满足这个需求,先列几个常用的:
```python
traceback.print_exc() # 打印错误类型、值和具体的trace信息
traceback.print_exception(type, value, traceback[, limit[, file]]) # 前三个参数的值可以从sys.exc_info()
raceback.print_exc([limit[, file]]) # 同上,不需要传入那么多参数
traceback.format_exc([limit]) # 同 print_exc(),返回的是字符串
traceback.extract_stack([file, [, limit]]) # 从当前栈中提取 trace 信息
```
traceback 模块获取异常相关的数据是通过sys.exc_info()得到的,该函数返回异常类型type、异常value、调用和堆栈信息traceback组成的元组。
同时 inspect 模块也提供了获取 traceback 对象的接口。
### 建议 47:使用 logging 记录日志信息
仅仅将信息输出到控制台是远远不够的,更为常见的是使用日志保存程序运行过程中的相关信息,如运行时间、描述信息以及错误或者异常发生时候的特定上下文信息。Python 提供 logging 模块提供了日志功能。
常规日志设置:
```python
logging.basicConfig(
filename='%s.log' % self.table_name,
level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S')
```
logging是线程安全的,不支持多进程写入同一个子文件,对多个进程需要配置不同的日志文件。
### 建议 48:使用 threading 模块编写多线程程序
(python3中,使用threadpool线程池模块比较省心)
由于 GIL 的存在,让 Python 多线程编程在多核处理器中无法发挥优势,但在一些使用场景下使用多线程仍然比较好,如等待外部资源返回,或建立反应灵活的用户界面,或多用户程序等。
Python3 提供了两个模块:_thread和threading。_thread提供了底层的多线程支持,使用比较复杂,下面我们重点说说threading。
Python 多线程支持用两种方式来创建线程:一种通过继承 Thread 类,重写它的run()方法;另一种是创建一个 threading.Thread 对象,在它的初始化函数__init__()中将可调用对象作为参数传入。
threading模块中不仅有 Lock 指令锁,RLock 可重入指令锁,还支持条件变量 Condition、信号量 Semaphore、BoundedSemaphore 以及 Event 事件等。
下面有一个比较经典的例子来理解多线程:
```python
import threading
from time import ctime,sleep
def music(func):
for i in range(2):
print("I was listening to %s. %s" % (func,ctime()))
sleep(1) # 程序休眠 1 秒
def move(func):
for i in range(2):
print("I was at the %s! %s" % (func,ctime()))
sleep(5)
threads = []
t1 = threading.Thread(target=music,args=('爱情买卖',))
threads.append(t1)
t2 = threading.Thread(target=move,args=('阿凡达',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
t.setDaemon(True) # 声明线程为守护线程
t.start()
#3
print("all over %s" % ctime())
```
以下是运行结果:
```python
I was listening to 爱情买卖. Tue Apr 4 17:57:02 2017
I was at the 阿凡达! Tue Apr 4 17:57:02 2017
all over Tue Apr 4 17:57:02 2017
```
分析:threading 模块支持线程守护,我们可以通过setDaemon()来设置线程的daemon属性,当其属性为True时,表明主线程的退出可以不用等待子线程完成,反之,daemon属性为False时所有的非守护线程结束后主线程才会结束,那运行结果为:
```python
I was listening to 爱情买卖. Tue Apr 4 18:05:26 2017
I was at the 阿凡达! Tue Apr 4 18:05:26 2017
all over Tue Apr 4 18:05:26 2017
I was listening to 爱情买卖. Tue Apr 4 18:05:27 2017
I was at the 阿凡达! Tue Apr 4 18:05:31 2017
```
继续修改代码,当我们在#3处加入t.join(),此方法能够阻塞当前上下文环境,直到调用该方法的线程终止或到达指定的 timeout,此时在运行程序:
```python
I was listening to 爱情买卖. Tue Apr 4 18:08:15 2017
I was at the 阿凡达! Tue Apr 4 18:08:15 2017
I was listening to 爱情买卖. Tue Apr 4 18:08:16 2017
I was at the 阿凡达! Tue Apr 4 18:08:20 2017
all over Tue Apr 4 18:08:25 2017
```
当我们把music函数的休眠时间改为 4 秒,再次运行程序:
```python
I was listening to 爱情买卖. Tue Apr 4 18:11:16 2017
I was at the 阿凡达! Tue Apr 4 18:11:16 2017
I was listening to 爱情买卖. Tue Apr 4 18:11:20 2017
I was at the 阿凡达! Tue Apr 4 18:11:21 2017
all over Tue Apr 4 18:11:26 2017
```
此时我们就可以发现多线程的威力了,music虽然增加了 3 秒,然而总的运行时间仍然为 10 秒。
### 建议 49:使用 Queue 使多线程编程更加安全
(同47,使用threadingpool)
线程间的同步和互斥,线程间数据的共享等这些都是涉及线程安全要考虑的问题。纵然 Python 中提供了众多的同步和互斥机制,如 mutex、condition、event 等,但同步和互斥本身就不是一个容易的话题,稍有不慎就会陷入死锁状态或者威胁线程安全。
如何保证线程安全呢?我们先来看看 Python 中的 Queue 模块:
* Queue.Queue(maxsize):先进先出,maxsize 为队列大小,其值为非正数的时候为无限循环队列
* Queue.LifoQueue(maxsize):后进先出,相当于栈
* Queue.PriorityQueue(maxsize):优先级队列
以上队列所支持的方法:
* Queue.qsize():返回近似的队列大小。当该值 > 0 的时候并不保证并发执行的时候 get() 方法不被阻塞,同样,对于 put() 方法有效。
* Queue.empty():队列为空的时候返回 True,否则返回 False
* Queue.full():当设定了队列大小的情况下,如果队列满则返回 True,否则返回 False
* Queue.put(item[, block[, timeout]]):往队列中添加元素 item,block 设置为 False 的时候,如果队列满则抛出 Full 异常。如果 block 设置为 True,timeout 为 None 的时候则会一直等待直到有空位置,否则会根据 timeout 的设定超时后抛出 Full 异常
* Queue.put_nowait(item):等于 put(item, False).block 设置为 False 的时候,如果队列空则抛出 Empty 异常。如果 block 设置为 True、timeout 为 None 的时候则会一直等到有元素可用,否则会根据 timeout 的设定超时后抛出 Empty 异常
* Queue.get([block[, timeout]]):从队列中删除元素并返回该元素的值
* Queue.get_nowait():等价于 get(False)
* Queue.task_done():发送信号表明入列任务已经完成,经常在消费者线程中用到
* Queue.join():阻塞直至队列中所有的元素处理完毕
首先 Queue 中的队列和 collections.deque 所表示的队列并不一样,前者用于不同线程之间的通信,内部实现了线程的锁机制,后者是数据结构上的概念,支持 in 方法。
Queue 模块实现了多个生产者多个消费者的队列,当多线程之间需要信息安全的交换的时候特别有用,因此这个模块实现了所需要的锁原语,为 Python 多线程编程提供了有力的支持,它是线程安全的。
先来看一个简单的例子:
```python
import os
import Queue
import threading
import urllib2
class DownloadThread(threading.Thead):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
url = self.queue.get()
print('{0} begin download {1}...'.format(self.name, url))
self.download_file(url)
self.queque.task_done()
print('{0} download completed!!!'.format(self.name))
def download_file(self, url):
urlhandler = urllib2.urlopen(url)
fname = os.path.basename(url) + '.html'
with open(fname, 'wb') as f:
while True:
chunk = urlhandler.read(1024)
if not chunk: break
f.write(chunk)
if __name__ == '__main__':
urls = ['http://wiki.python.org/moin/WebProgramming',
'https://www.createspace.com/3611970',
'http://wiki.python.org/moin/Documentation'
]
queue = Queue.Queue()
for i range(5):
t = DownloadThread(queue)
t.setDaemon(True)
t.start()
for url in urls:
queue.put(url)
queue.join()
```
## 第 5 章 设计模式
### 建议 50:利用模块实现单例模式
单例模式可以保证徐彤中一个类只有一个实例且该实例易被外界访问,常用来使用XxxManager之类的功能。
满足单例模式的 3 个需求:
* 只能有一个实例
* 必须自行创建这个实例
* 必须自行向整个系统提供这个实例
模块采用的其实是天然的单例的实现方式,在入口文件导入:
* 所有的变量都会绑定到模块
* 模块只初始化一次
* import 机制是线程安全的,保证了在并发状态下模块也只是一个实例
```python
# World.py
import Sun
def run():
while True:
Sun.rise()
Sun.set()
# main.py
import World
World.run()
```
此外,Borg模式可以创造任意数量实例,并保证状态共享。
### 建议 51:用 mixin 模式让程序更加灵活
模板方法模式就是在一个方法中定义一个算法的骨架,并将一些实现步骤延迟到子类中。模板方法可以使子类在不改变算法结构的情况下,重新定义算法中的某些步骤。
```python
class UseSimpleTeapot(object):
def get_teapot(self):
return SimpleTeapot()
class UseKungfuTeapot(object):
def get_teapot(self):
return KungfuTeapot()
class OfficePeople(People, UseSimpleTeapot): pass
class HomePeople(People, UseSimpleTeapot): pass
class Boss(People, UseKungfuTeapot): pass
def simple_tea_people():
people = People()
people.__base__ += (UseSimpleTeapot,)
return people
def coffee_people():
people = People()
people.__base__ += (UseCoffeepot,)
def tea_and_coffee_people():
people = People()
people.__base__ += (UseSimpleTeapot, UserCoffeepot,)
return people
def boss():
people = People()
people.__base__ += (KungfuTeapot, UseCoffeepot, )
return people
```
代码的原理在于每个类都有一个__bases__属性,它是一个元组,用来存放所有的基类,作为动态语言,Python 中的基类可以在运行中可以动态改变。所以当我们向其中增加新的基类时,这个类就拥有了新的方法,这就是混入mixin。
利用这个技术我们可以在不修改代码的情况下就可以完成需求:
```python
import mixins # 把员工需求定义在 Mixin 中放在 mixins 模块
def staff():
people = People()
bases = []
for i in config.checked():
bases.append(getattr(maxins, i))
people.__base__ += tuple(bases)
return people
```
### 建议 52:用发布订阅模式实现松耦合
发布订阅模式是一种编程模式,消息的发送者不会发送其消息给特定的接收者,而是将发布的消息分为不同的类别直接发布,并不关注订阅者是谁。而订阅者可以对一个或多个类别感兴趣,且只接收感兴趣的消息,并且不关注是哪个发布者发布的消息。要实现这个模式,就需要一个中间代理人。 Broker,它维护着发布者和订阅者的关系,订阅者把感兴趣的主题告诉它,而发布者的信息也通过它路由到各个订阅者处。
```python
from collections import defaultdict
route_table = defaultdict(list)
def sub(topic, callback):
if callback in route_table[topic]:
return
route_table[topic].append(callback)
def pub(topic, *args, **kw):
for func in route_table[topic]:
func(*args, **kw)
```
将以上代码放在 Broker.py 的模块,省去了各种参数检测、优先处理、取消订阅的需求,只向我们展示发布订阅模式的基础实现:
```python
import Broker
def greeting(name):
print('Hello, {}'.format(name))
Broker.sub('greet', greeting)
Broker.pub('greet', 'LaiYonghao')

因为python-message的消息订阅默认是全局性的,所以有可能产生名字冲突。

建议 53:用状态模式美化代码

所谓状态模式,就是当一个对象的内在状态改变时允许改变其行为,但这个对象看起来像是改变了其类。

简单的状态模式有其缺点:

  • 查询对象的当前状态很麻烦
  • 状态切换时需要对原状态做一些清扫工作,而对新状态做初始化工作,因每个状态需要做的事情不同,全部写在切换状态的代码中必然重复

这时候我们可以使用 Python-state 来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from state import curr, switch, stateful, State, behavior
@stateful
class People(object):
class Workday(State):
default = True
@behavior # 相当于staticmethod
def day(self): # 这里的self并不是Python的关键字,而是有助于我们理解状态类的宿主是People的实例
print('work hard')
class Weekend(State):
@behavior
def day(self):
print('play harder')
people = People()
while True:
for i in range(1, 8):
if i == 6:
switch(people, People.Weekend)
if i == 1:
switch(people, People.Workday)
people.day()

@statefule装饰器重载了被修饰的类的__getattr__()从而使得 People 的实例能够调用当前状态类的方法,同时被修饰的类的实例是带有状态的,能够使用curr()查询当前状态,也可以使用switch()进行状态切换,默认的状态是通过类定义的 default 属性标识,default = True的类成为默认状态。

状态类 Workday 和 Weekend 继承自 State 类,从其派生的子类可以使用__begin__和__end___状态转换协议,自定义进入和离开当前状态时对宿主的初始化和清理工作。

下面是一个真实业务的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@stateful
class User(object):
class NeedSignin(State):
default = True
@behavior
def signin(self, user, pwd):
...
switch(self, Player.Signin)
class Signin(State):
@behavior
def move(self, dst): ...
@behavior
def atk(self, other): ...

第 6 章 内部机制

建议 54:理解 built-in objects

Python 中一切皆对象,在新式类中,object 是所有内建类型的基类,用户自定义的类可以继承自 object 也可继承自内建类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [1]: class TestNewClass:
...: __metaclass__ = type
...:
In [2]: type(TestNewClass)
Out[2]: type
In [3]: TestNewClass.__bases__
Out[3]: (object,)
In [4]: a = TestNewClass()
In [5]: type(a)
Out[5]: __main__.TestNewClass
In [6]: a.__class__
Out[6]: __main__.TestNewClass

新式类支持 property 和描述符特性,作为新式类的祖先,Object 类还定义了一些特殊方法:new()、init()、delattr()、getattribute()、setattr()、hash()、repr()、str()等。

建议 55:init()不是构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A(object):
def __new__(cls, *args, **kw):
print(cls)
print(args)
print(kw)
print('----------')
instance = object.__new__(cls, *args, **kw)
print(instance)
def __init__(self, a, b):
print('init gets called')
print('self is {}'.format(self))
self.a, self.b = a, b
a1 = A(1, 2)
print(a1.a)
print(a1.b)

运行结果:

1
2
3
4
5
6
7
8
9
10
<class '__main__.A'>
(1, 2)
{}
----------
Traceback (most recent call last):
File "test.py", line 19, in <module>
a1 = A(1, 2)
File "test.py", line 13, in __new__
instance = object.__new__(cls, *args, **kw)
TypeError: object() takes no parameters

从结果中我们可以看出,程序输出了__new__()调用所产生的输出,并抛出了异常。于是我们知道,原来__new__()才是真正创建实例,是类的构造方法,而__init__()是在类的对象创建好之后进行变量的初始化。上面程序抛出异常是因为在__new__()中没有显式返回对象,a1此时为None,当去访问实例属性时就抛出了异常。

根据官方文档,我们可以总结以下几点:

  • object.new(cls[, args…]):其中 cls 代表类,args 为参数列表,为静态方法
  • object.init(self[, args…]):其中 self 代表实例对象,args 为参数列表,为实例方法
  • 控制实例创建的时候可使用 new() ,而控制实例初始化的时候使用 init()
  • new()需要返回类的对象,当返回类的对象时将会自动调用__init__()进行初始化,没有对象返回,则__init__()不会被调用。init() 方法不需要显示返回,默认为 None,否则会在运行时抛出 TypeError
  • 但当子类继承自不可变类型,如 str、int、unicode 或者 tuple 的时候,往往需要覆盖__new__()
  • 覆盖 new() 和 init() 的时候这两个方法的参数必须保持一致,如果不一致将导致异常
    下面我们来总结需要覆盖__new__()的几种特殊情况:
  • 当类继承不可变类型且默认的 new() 方法不能满足需求的时候
  • 用来实现工厂模式或者单例模式或者进行元类编程,使用__new__()来控制对象创建
  • 作为用来初始化的 init() 方法在多继承的情况下,子类的 init()方法如果不显式调用父类的 init() 方法,则父类的 init() 方法不会被调用;通过super(子类, self).init()显式调用父类的初始化方法;对于多继承的情况,我们可以通过迭代子类的 bases 属性中的内容来逐一调用父类的初始化方法

分别来看例子加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 创建一个集合能够将任何以空格隔开的字符串变为集合中的元素
class UserSet(frozenset):
def __new__(cls, *args):
if args and isinstance(args[0], str):
args = (args[0].split(), ) + args[1:]
return super(UserSet, cls).__new__(cls, *args)
# 一个工厂类根据传入的参量决定创建出哪一种产品类的实例
class Shape(object):
def __init__(object):
pass
def draw(self):
pass
class Triangle(Shape):
def __init__(self):
print("I am a triangle")
def draw(self):
print("I am drawing triangle")
class Rectangle(Shape):
def __init__(self):
print("I am a rectangle")
def draw(self):
print("I am drawing triangle")
class Trapezoid(Shape):
def __init__(self):
print("I am a trapezoid")
def draw(self):
print("I am drawing triangle")
class Diamond(Shape):
def __init__(self):
print("I am a diamond")
def draw(self):
print("I am drawing triangle")
class ShapeFactory(object):
shapes = {'triangle': Triangle, 'rectangle': Rectangle, 'trapzoid': Trapezoid, 'diamond': Diamond}
def __new__(cls, name):
if name in ShapeFactory.shapes.keys():
print('creating a new shape {}'.format(name))
return ShapeFactory.shapes[name]()
else:
print('creating a new shape {}'.format(name))
return Shape()

建议 56:理解名字查找机制

在 Python 中所谓的变量其实都是名字,这些名字指向一个或多个 Python 对象。这些名字都存在于一个表中(命名空间),我们称之为局部变量,调用locals()可以查看:

1
2
3
4
>>> locals()
{'__package__': None, '__spec__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__doc__': None, '__name__': '__main__', '__builtins__': <module 'builtins' (built-in)>}
>>> globals()
{'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__doc__': None, '__spec__': None, '__name__': '__main__'}

Python 中的作用域分为:

  • 局部作用域: 一般来说函数的每次调用都会创建一个新的本地作用域, 拥有新的命名空间
  • 全局作用域: 定义在 Python 模块文件中的变量名拥有全局作用域, 即在一个文件的顶层的变量名仅在这个文件内可见
  • 嵌套作用域: 多重函数嵌套时才会考虑, 即使使用 global 进行申明也不能达到目的, 其结果最终是在嵌套的函数所在的命名空间中创建了一个新的变量
  • 内置作用域: 通过标准库中的__builtin__实现的 当访问一个变量的时候,其查找顺序遵循变量解析机制 LEGB 法则,即依次搜索 4 个作用域:局部作用域、嵌套作用域、全局作用域以及内置作用域,并在第一个找到的地方停止搜寻,如果没有搜到,则会抛出异常。

Python 3 中引入了 nonlocal 关键字:

1
2
3
4
5
6
7
8
def foo(x):
a = x
def bar():
nonlocal a
b = a * 2
a = b + 1
print(a)
return bar

建议 57: 为什么需要 self 参数

在类中当定义实例方法的时候需要将第一个参数显式声明为self, 而调用时不需要传入该参数, 我们通过self.x访问实例变量, self.m()访问实例方法:

1
2
3
4
5
6
7
8
9
10
11
class SelfTest(object):
def __init__(self.name):
self.name = name
def showself(self):
print('self here is {}'.format(self))
def display(self):
self.showself()
print('The name is: {}'.format(self.name))
st = SelfTest('instance self')
st.display()
print('{}'.format(st))

运行结果:

1
2
3
self here is <__main__.SelfTest object at 0x7f440c53ba58>
The name is: instance self
<__main__.SelfTest object at 0x7f440c53ba58>

从中可以发现, self 表示实例对象本身, 即 SelfTest 类的对象在内存中的地址. self 是对对象 st 本身的引用, 我们在调用实例方法时也可以直接传入实例对象: SelfTest.display(st). 同时 self 或 cls 并不是 Python 的关键字, 可以替换成其它的名称。

Python 中为什么需要 self 呢:

  1. 借鉴了其他语言的特征
  2. Python 语言本身的动态性决定了使用 self 能够带来一定便利
  3. 在存在同名的局部变量以及实例变量的情况下使用 self 使得实例变量更容易被区分

Python 属于一级对象语言, 我们有好几种方法可以引用类方法:

1
2
A.__dict__["m"]
A.m.__func__

Python 的哲学是:显示优于隐式(Explicit is better than implicit)。

建议 58: 理解 MRO 与多继承

古典类与新式类所采取的 MRO (Method Resolution Order, 方法解析顺序) 的实现方式存在差异。

古典类是按照多继承申明的顺序形成继承树结构, 自顶向下采用深度优先的搜索顺序. 而新式类采用的是 C3 MRO 搜索方法, 在新式类通过__mro__得到 MRO 的搜索顺序, C3 MRO 的算法描述如下:

假定,C1C2…CN 表示类 C1 到 CN 的序列,其中序列头部元素(head)=C1,序列尾部(tail)定义 = C2…CN;

C 继承的基类自左向右分别表示为 B1,B2…BN

L[C] 表示 C 的线性继承关系,其中 L[object] = object。

算法具体过程如下:

L[C(B1…BN)] = C + merge(L[B1] … L[BN], B1 … BN)

其中 merge 方法的计算规则如下:在 L[B1]…L[BN],B1…BN 中,取 L[B1] 的 head,如果该元素不在 L[B2]…L[BN],B1…BN 的尾部序列中,则添加该元素到 C 的线性继承序列中,同时将该元素从所有列表中删除(该头元素也叫 good head),否则取 L[B2] 的 head。继续相同的判断,直到整个列表为空或者没有办法找到任何符合要求的头元素(此时,将引发一个异常)。

菱形继承是我们在多继承设计的时候需要尽量避免的一个问题。

建议 59: 理解描述符机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
In [1]: class MyClass(object):
...: class_attr = 1
...:
# 每一个类都有一个__dict__属性, 包含它的所有属性
In [2]: MyClass.__dict__
Out[2]:
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
'class_attr': 1})
In [3]: my_instance = MyClass()
# 每一个实例也相应有一个实例属性, 我们通过实例访问一个属性时,
# 它首先会尝试在实例属性中查找, 找不到会到类属性中查找
In [4]: my_instance.__dict__
Out[4]: {}
# 实例访问类属性
In [5]: my_instance.class_attr
Out[5]: 1
# 如果通过实例增加一个属性,只能改变此实例的属性
In [6]: my_instance.inst_attr = 'china'
In [7]: my_instance.__dict__
Out[7]: {'inst_attr': 'china'}
# 对于类属性而言并没有丝毫变化
In [8]: MyClass.__dict__
Out[8]:
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
'class_attr': 1})
# 我们可以动态地给类增加一个属性
In [9]: MyClass.class_attr2 = 100
In [10]: my_instance.class_attr2
Out[10]: 100
# 但Python的内置类型并不能随意地为它增加属性或方法

.操作符封装了对实例属性和类属性两种不同属性进行查找的细节。

但是如果是访问方法呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [1]: class MyClass(object):
...: def my_method(self):
...: print('my_method')
...:
In [2]: MyClass.__dict__['my_method']
Out[2]: <function __main__.MyClass.my_method>
In [3]: MyClass.my_method
Out[3]: <function __main__.MyClass.my_method>
In [4]: type(MyClass.my_method)
Out[4]: function
In [5]: type(MyClass.__dict__['my_method'])
Out[5]: function

根据通过实例访问属性和根据类访问属性的不同,有以下两种情况:

  • 一种是通过实例访问,比如代码 obj.x,如果 x 是一个描述符,那么 getattribute() 会返回 type(obj).dict[‘x’].get(obj, type(obj)) 结果,即:type(obj) 获取 obj 的类型;type(obj).dict[‘x’] 返回的是一个描述符,这里有一个试探和判断的过程;最后调用这个描述符的 get() 方法。

  • 另一个是通过类访问的情况,比如代码 cls.x,则会被 getattribute()转换为 cls.dict[‘x’].get(None, cls)。

    描述符协议是一个 Duck Typing 的协议,而每一个函数都有 get 方法,也就是说其他每一个函数都是描述符。所有对属性, 方法进行修饰的方案往往都用到了描述符, 如classmethod, staticmethod, property等, 以下是property的参考实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
    self.fget = fget
    self.fset = fset
    self.fdel = fdel
    self.__doc__ = doc
    def __get__(self, obj, objtype=None):
    if obj is None:
    return self
    if self.fget is None:
    raise AttributeError, "unreadable attribute"
    return self.fget(obj)
    def __set__(self, obj, value):
    if self.fset is None:
    raise AttributeError, "can't set attribute"
    self.fset(obj, value)
    def __delete__(self, obj):
    if self.fdel is None:
    raise AttributeError, "can't delete attribute"
    self.fdel(obj)

建议 60:区别__getattr__()和__getattribute__()方法

以上两种方法可以对实例属性进行获取和拦截:

  • getattr(self, name):适用于属性在实例中以及对应的类的基类以及祖先类中都不存在;
  • getattribute(self, name):对于所有属性的访问都会调用该方法。

但访问不存在的实例属性时,会由内部方法__getattribute__()抛出一个 AttributeError 异常,也就是说只要涉及实例属性的访问就会调用该方法,它要么返回实际的值,要么抛出异常。详情请参考

那么__getattr__()在什么时候调用呢:

  • 属性不在实例的__dict__中;
  • 属性不在其基类以及祖先类的__dict__中;
  • 触发AttributeError异常时(注意,不仅仅是__getattribute__()方法的AttributeError异常,property 中定义的get()方法抛出异常的时候也会调用该方法)。

当这两个方法同时被定义的时候,要么在__getattribute__()中显式调用,要么触发AttributeError异常,否则__getattr__()永远不会被调用。

我们知道 property 也能控制属性的访问,如果一个类中如果定义了 property、getattribute()以及__getattr__()来对属性进行访问控制,会最先搜索__getattribute__()方法,由于 property 对象并不存在于 dict 中,因此并不能返回该方法,此时会搜索 property 中的get()方法;当 property 中的set()方法对属性进行修改并再次访问 property 的get()方法会抛出异常,这时会触发__getattr__()的调用。

getattribute()总会被调用,而__getattr__()只有在__getattribute__()中引发异常的情况下调用。